package bsh.dyn;

import java.lang.reflect.Array;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import bsh.CompilationState;
import bsh.CompilationUnit;
import bsh.CompiledScript;
import bsh.exception.CompilationFailedException;
import bsh.meta.JavaClassWrapper;
import bsh.utils.reflection.BytecodeHelper;

/**
 * {@link ArgumentsAdaptor} that has as its last argument a 'varargs'
 * 
 * @author takeshi
 */
public class VarArgsArgumentAdaptor implements ArgumentsAdaptor {

    private final ArgumentsAdaptorImpl regularAdaptor;

    private final Class<?>[] regularParameters;

    private final Class<?> varArgsType; // should be an array

    private final TransformationList[] transformations;

    private final int weight;

    /**
     * C'tor
     * 
     * @param regularAdaptor the adaptor for the 'regular' arguments
     * @param varArgsType the type of the varArgs. Must be an array type
     * @param transformations the tranformations for the arguments of the varargs
     */
    public VarArgsArgumentAdaptor(ArgumentsAdaptorImpl regularAdaptor, Class<?> varArgsType, TransformationList[] transformations) {
        super();
        this.regularAdaptor = regularAdaptor;
        this.regularParameters = this.regularAdaptor.targetTypes();
        this.varArgsType = varArgsType;
        if (!varArgsType.isArray()) {
            throw new IllegalArgumentException();
        }
        this.transformations = transformations;
        this.weight = regularAdaptor.weight() + ArgumentsAdaptorImpl.calculateWeight(transformations);
    }

    /**
     * {@inheritDoc}
     */
    public Class<?>[] targetTypes() {
        Class<?>[] copy = new Class<?>[this.regularParameters.length + 1];
        System.arraycopy(this.regularParameters, 0, copy, 0, copy.length);
        copy[regularParameters.length] = varArgsType;
        return copy;
    }

    /**
     * {@inheritDoc}
     */
    public Object[] adaptArguments(CompiledScript script, Object[] original) {
        Object[] copy = new Object[this.regularParameters.length + 1];
        adaptArgumentsInto(script, original, copy);
        return copy;
    }

    /**
     * {@inheritDoc}
     */
    public void adaptArgumentsInto(CompiledScript script, Object[] original, Object[] destination) {
        this.regularAdaptor.adaptArgumentsInto(script, original, destination);
        int length = original.length - destination.length + 1;
        Object newArray = Array.newInstance(this.varArgsType.getComponentType(), length);
        for (int i = 0; i < length; i++) {
            Object adapted = this.transformations[i].transform(original[i + destination.length - 1], script);
            Array.set(newArray, i, adapted);
        }
        destination[destination.length - 1] = newArray;
    }

    /**
     * {@inheritDoc}
     */
    public int weight() {
        return weight;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isCompilable() {
        if (!this.regularAdaptor.isCompilable()) {
            return false;
        }
        for (TransformationList list : this.transformations) {
            if (!list.isCompilable()) {
                return false;
            }
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public void generateBytecode(MethodVisitor visitor, CompilationUnit unit, CompilationState state) throws CompilationFailedException {
        this.regularAdaptor.generateBytecode(visitor, unit, state);
        Class<?> varArgsComponentType = this.varArgsType.getComponentType();
        Type componentType = Type.getType(varArgsComponentType);

        int old = state.getStackSize();
        BytecodeHelper.pushIntConst(this.transformations.length, visitor);

        ((JavaClassWrapper) JavaClassWrapper.wrapperFor(varArgsComponentType)).allocatePrimitiveArray(visitor);

        state.pushStack(this.varArgsType);
        for (int i = 0; i < this.transformations.length; i++) {
            // args E[]
            int size = state.getStackSize();

            visitor.visitInsn(Opcodes.DUP);
            state.pushStack(this.varArgsType);
            // args E[] E[]
            BytecodeHelper.pushIntConst(i, visitor);
            state.pushStack(int.class);
            // args E[] E[] int

            ArgumentsAdaptorImpl.loadParameterWithIndex(visitor, this.regularParameters.length + i, state);
            // args E[] E[] int Object

            this.transformations[i].compile(visitor, state);
            // args E[] E[] int E
            state.assertStackTopType(JavaClassWrapper.wrapperFor(varArgsComponentType));
            visitor.visitInsn(componentType.getOpcode(Opcodes.IASTORE));
            // args E[]
            final int toPop = 3;
            state.popStack(toPop);
            state.assertStackSize(size);
        }
        state.assertStackSize(old + 1);
        state.assertStackTopType(JavaClassWrapper.wrapperFor(this.varArgsType));
    }

}
